// Package tryfunc contains some optional functions that can be exposed in
// HCL-based languages to allow authors to test whether a particular expression
// can succeed and take dynamic action based on that result.
//
// These functions are implemented in terms of the customdecode extension from
// the sibling directory "customdecode", and so they are only useful when
// used within an HCL EvalContext. Other systems using cty functions are
// unlikely to support the HCL-specific "customdecode" extension.
package tryfunc

import (
    "errors"
    "fmt"
    "strings"

    "Havoc/pkg/profile/yaotl"
    "Havoc/pkg/profile/yaotl/ext/customdecode"
    "github.com/zclconf/go-cty/cty"
    "github.com/zclconf/go-cty/cty/function"
)

// TryFunc is a variadic function that tries to evaluate all of is arguments
// in sequence until one succeeds, in which case it returns that result, or
// returns an error if none of them succeed.
var TryFunc function.Function

// CanFunc tries to evaluate the expression given in its first argument.
var CanFunc function.Function

func init() {
    TryFunc = function.New(&function.Spec{
        VarParam: &function.Parameter{
            Name: "expressions",
            Type: customdecode.ExpressionClosureType,
        },
        Type: func(args []cty.Value) (cty.Type, error) {
            v, err := try(args)
            if err != nil {
                return cty.NilType, err
            }
            return v.Type(), nil
        },
        Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
            return try(args)
        },
    })
    CanFunc = function.New(&function.Spec{
        Params: []function.Parameter{
            {
                Name: "expression",
                Type: customdecode.ExpressionClosureType,
            },
        },
        Type: function.StaticReturnType(cty.Bool),
        Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
            return can(args[0])
        },
    })
}

func try(args []cty.Value) (cty.Value, error) {
    if len(args) == 0 {
        return cty.NilVal, errors.New("at least one argument is required")
    }

    // We'll collect up all of the diagnostics we encounter along the way
    // and report them all if none of the expressions succeed, so that the
    // user might get some hints on how to make at least one succeed.
    var diags hcl.Diagnostics
    for _, arg := range args {
        closure := customdecode.ExpressionClosureFromVal(arg)
        if dependsOnUnknowns(closure.Expression, closure.EvalContext) {
            // We can't safely decide if this expression will succeed yet,
            // and so our entire result must be unknown until we have
            // more information.
            return cty.DynamicVal, nil
        }

        v, moreDiags := closure.Value()
        diags = append(diags, moreDiags...)
        if moreDiags.HasErrors() {
            continue // try the next one, if there is one to try
        }
        return v, nil // ignore any accumulated diagnostics if one succeeds
    }

    // If we fall out here then none of the expressions succeeded, and so
    // we must have at least one diagnostic and we'll return all of them
    // so that the user can see the errors related to whichever one they
    // were expecting to have succeeded in this case.
    //
    // Because our function must return a single error value rather than
    // diagnostics, we'll construct a suitable error message string
    // that will make sense in the context of the function call failure
    // diagnostic HCL will eventually wrap this in.
    var buf strings.Builder
    buf.WriteString("no expression succeeded:\n")
    for _, diag := range diags {
        if diag.Subject != nil {
            buf.WriteString(fmt.Sprintf("- %s (at %s)\n  %s\n", diag.Summary, diag.Subject, diag.Detail))
        } else {
            buf.WriteString(fmt.Sprintf("- %s\n  %s\n", diag.Summary, diag.Detail))
        }
    }
    buf.WriteString("\nAt least one expression must produce a successful result")
    return cty.NilVal, errors.New(buf.String())
}

func can(arg cty.Value) (cty.Value, error) {
    closure := customdecode.ExpressionClosureFromVal(arg)
    if dependsOnUnknowns(closure.Expression, closure.EvalContext) {
        // Can't decide yet, then.
        return cty.UnknownVal(cty.Bool), nil
    }

    _, diags := closure.Value()
    if diags.HasErrors() {
        return cty.False, nil
    }
    return cty.True, nil
}

// dependsOnUnknowns returns true if any of the variables that the given
// expression might access are unknown values or contain unknown values.
//
// This is a conservative result that prefers to return true if there's any
// chance that the expression might derive from an unknown value during its
// evaluation; it is likely to produce false-positives for more complex
// expressions involving deep data structures.
func dependsOnUnknowns(expr hcl.Expression, ctx *hcl.EvalContext) bool {
    for _, traversal := range expr.Variables() {
        val, diags := traversal.TraverseAbs(ctx)
        if diags.HasErrors() {
            // If the traversal returned a definitive error then it must
            // not traverse through any unknowns.
            continue
        }
        if !val.IsWhollyKnown() {
            // The value will be unknown if either it refers directly to
            // an unknown value or if the traversal moves through an unknown
            // collection. We're using IsWhollyKnown, so this also catches
            // situations where the traversal refers to a compound data
            // structure that contains any unknown values. That's important,
            // because during evaluation the expression might evaluate more
            // deeply into this structure and encounter the unknowns.
            return true
        }
    }
    return false
}
